<?php

class Compiler extends Template {
	// public configuration variables
	var $left_tag			= "";
	var $right_tag			= "";
	var $plugin_dir			= "";
	var $template_dir		= "";

	// private internal variables
	var $_vars				=	array();	// stores all internal assigned variables
	var $_linenum			=	0;		// the current line number in the file we are processing
	var $_file				=	"";		// the current file we are processing
	var $_literal			=	array();	// stores all literal blocks
	var $_foreachelse_stack	=	array();
	var $_for_stack			=	0;
	var $_switch_stack		=	array();
	var $_tag_stack			=	array();
	var $_require_stack		=	array();	// stores all files that are "required" inside of the template
	var $_error_level		=	null;
	var $_sl_md5			=	'39fc70570b8b60cbc1b85839bf242aff';

	var $_db_qstr_regexp	=	null;		// regexps are setup in the constructor
	var $_si_qstr_regexp	=	null;
	var $_qstr_regexp		=	null;
	var $_func_regexp		=	null;
	var $_var_bracket_regexp=	null;
	var $_dvar_regexp		=	null;
	var $_cvar_regexp		=	null;
	var $_mod_regexp		=	null;
	var $_var_regexp		=	null;

	function compiler() {
		// matches double quoted strings:
		// "foobar"
		// "foo\"bar"
		// "foobar" . "foo\"bar"
		$this->_db_qstr_regexp = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';

		// matches single quoted strings:
		// 'foobar'
		// 'foo\'bar'
		$this->_si_qstr_regexp = '\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\'';

		// matches single or double quoted strings
		$this->_qstr_regexp = '(?:' . $this->_db_qstr_regexp . '|' . $this->_si_qstr_regexp . ')';

		// matches bracket portion of vars
		// [0]
		// [foo]
		// [$bar]
		// [#bar#]
		$this->_var_bracket_regexp = '\[[\$|\#]?\w+\#?\]';
				
		// matches $ vars (not objects):
		// $foo
		// $foo[0]
		// $foo[$bar]
		// $foo[5][blah]
		$this->_dvar_regexp = '\$[a-zA-Z0-9_]{1,}(?:' . $this->_var_bracket_regexp . ')*(?:' . $this->_var_bracket_regexp . ')*';

		// matches config vars:
		// #foo#
		// #foobar123_foo#
		$this->_cvar_regexp = '\#[a-zA-Z_]{1,}(?:' . $this->_var_bracket_regexp . ')*(?:' . $this->_var_bracket_regexp . ')*\#';

		// matches valid variable syntax:
		// $foo
		// 'text'
		// "text"
		$this->_var_regexp = '(?:(?:' . $this->_dvar_regexp . '|' . $this->_cvar_regexp . ')|' . $this->_qstr_regexp . ')';
		
		// matches valid modifier syntax:
		// |foo
		// |@foo
		// |foo:"bar"
		// |foo:$bar
		// |foo:"bar":$foobar
		// |foo|bar
		$this->_mod_regexp = '(?:\|@?[0-9a-zA-Z_]+(?::(?>-?\w+|' . $this->_dvar_regexp . '|' . $this->_qstr_regexp .'))*)';		

		// matches valid function name:
		// foo123
		// _foo_bar
		$this->_func_regexp = '[a-zA-Z0-9_]+';
	}

	function _compile_file($file_contents) {
		$ldq = preg_quote($this->left_tag);
		$rdq = preg_quote($this->right_tag);
		$_match		= array();		// a temp variable for the current regex match
		$tags		= array();		// all original tags
		$text		= array();		// all original text
		$compiled_text	= "";			// stores the compiled result
		$compiled_tags	= array();		// all tags and stuff

		// remove all comments
		$file_contents = preg_replace("!{$ldq}\*.*?\*{$rdq}!se","",$file_contents);

		// replace all php start and end tags
		$file_contents = preg_replace('%(<\?(?!php|=|$))%i', '<?php echo \'\\1\'?>'."\n", $file_contents);

		// remove literal blocks
		preg_match_all("!{$ldq}\s*literal\s*{$rdq}(.*?){$ldq}\s*/literal\s*{$rdq}!s", $file_contents, $_match);
		$this->_literal = $_match[1];
		$file_contents = preg_replace("!{$ldq}\s*literal\s*{$rdq}(.*?){$ldq}\s*/literal\s*{$rdq}!s", stripslashes($ldq . "literal" . $rdq), $file_contents);

		// gather all template tags
		preg_match_all("!{$ldq}\s*(.*?)\s*{$rdq}!s", $file_contents, $_match);
		$tags = $_match[1];

		// put all of the non-template tag text blocks into an array, using the template tags as delimiters
		$text = preg_split("!{$ldq}.*?{$rdq}!s", $file_contents);

		// compile template tags
		for ($i = 0, $for_max = count($tags); $i < $for_max; $i++) {
			$this->_linenum += substr_count($text[$i], "\n");
			$compiled_tags[] = $this->_compile_tag($tags[$i]);
			$this->_linenum += substr_count($tags[$i], "\n");
		}

		// build the compiled template by replacing and interleaving text blocks and compiled tags
		for ($i = 0, $for_max = count($compiled_tags); $i < $for_max; $i++)
			$compiled_text .= $text[$i].$compiled_tags[$i];
		$compiled_text .= $text[$i];

		// add the required requires
		while($add = array_pop($this->_require_stack))
			$compiled_text = '<?php require_once(\''. $this->_get_plugin_dir() . $add[0] . '\'); $this->register_' . $add[1] . '("' . $add[2] . '", "' . $add[3] . '"); ?>' . $compiled_text;

		// remove unnecessary close/open tags
		$compiled_text = preg_replace('!\?>\n?<\?php!', '', $compiled_text);

		return $compiled_text;
	}

	function _compile_tag($tag) {
		$_match		= array();		// stores the tags
		$_result	= "";			// the compiled tag
		$_variable	= "";			// the compiled variable

		// extract the tag command, modifier and arguments
		preg_match_all('/(?:(' . $this->_var_regexp . '|\/?' . $this->_func_regexp . ')(?:\s*[,\.]\s*)?)(?:\s+(.*))?/xs', $tag, $_match);

		if ($_match[1][0]{0} == '$' || ($_match[1][0]{0} == '#' && $_match[1][0]{strlen($_match[1][0]) - 1} == '#') || $_match[1][0]{0} == "'" || $_match[1][0]{0} == '"') {
			$_result = $this->_parse_variables($_match[1], $_match[2]);
			return "<?php echo $_result; ?>\n";
		}
		// process a function
		$tag_command = $_match[1][0];
		$tag_arguments = !empty($_match[2][0]) ? $_match[2][0] : null;
		$_result = $this->_parse_function($tag_command, $tag_arguments);
		return $_result;
	}

	function _parse_function($function, $arguments) {
		switch ($function) {
			case 'include':
				$_args = $this->_parse_arguments($arguments);
				if (!isset($_args['file']))
					$this->trigger_error("missing 'file' attribute in 'include'", E_USER_ERROR, __FILE__, __LINE__);
				if (!isset($_args['assign']))
					return '<?php echo $this->_fetch_compile(' . $_args['file'] . '); ?>';
				else
					return '<?php $this->assign("' . $this->_dequote($_args['assign']) . '", $this->_fetch_compile(' . $_args['file'] . ')); ?>';
				break;
			case 'ldelim':
				return $this->left_tag;
				break;
			case 'rdelim':
				return $this->right_tag;
				break;
			case 'literal':
				list (,$literal) = each($this->_literal);
				$this->_linenum += substr_count($literal, "\n");
				return "<?php echo '".str_replace("'", "\'", str_replace("\\", "\\\\", $literal))."'; ?>\n";
				break;
			case 'foreach':
				array_push($this->_foreachelse_stack, false);
				$_args = $this->_parse_arguments($arguments);
				if (!isset($_args['from']))
					$this->trigger_error("missing 'from' attribute in 'foreach'", E_USER_ERROR, __FILE__, __LINE__);
				if (!isset($_args['value']) && !isset($_args['item']))
					$this->trigger_error("missing 'value' attribute in 'foreach'", E_USER_ERROR, __FILE__, __LINE__);
				if (isset($_args['value']))
					$_args['value'] = $this->_dequote($_args['value']);
				elseif (isset($_args['item']))
					$_args['value'] = $this->_dequote($_args['item']);
				isset($_args['key']) ? $_args['key'] = "\$this->_vars['".$this->_dequote($_args['key'])."'] => " : $args['key'] = '';
				$_result = '<?php if (count((array)' . $_args['from'] . ')): foreach ((array)' . $_args['from'] . ' as ' . $_args['key'] . '$this->_vars[\'' . $_args['value'] . '\']): ?>';
				return $_result;
				break;
			case 'foreachelse':
				$this->_foreachelse_stack[count($this->_foreachelse_stack)-1] = true;
				return "<?php endforeach; else: ?>";
				break;
			case '/foreach':
				if (array_pop($this->_foreachelse_stack))
					return "<?php endif; ?>";
				else
					return "<?php endforeach; endif; ?>";
				break;
			case 'for':
				$this->_for_stack++;
				$_args = $this->_parse_arguments($arguments);
				if (!isset($_args['start']))
					$this->trigger_error("missing 'start' attribute in 'for'", E_USER_ERROR, __FILE__, __LINE__);
				if (!isset($_args['stop']))
					$this->trigger_error("missing 'stop' attribute in 'for'", E_USER_ERROR, __FILE__, __LINE__);
				if (!isset($_args['step']))
					$_args['step'] = 1;
				$_result = '<?php for($for' . $this->_for_stack . ' = ' . $_args['start'] . '; ((' . $_args['start'] . ' < ' . $_args['stop'] . ') ? ($for' . $this->_for_stack . ' < ' . $_args['stop'] . ') : ($for' . $this->_for_stack . ' > ' . $_args['stop'] . ')); $for' . $this->_for_stack . ' += ((' . $_args['start'] . ' < ' . $_args['stop'] . ') ? ' . $_args['step'] . ' : -' . $_args['step'] . ')): ?>';
				if (isset($_args['value']))
					$_result .= '<?php $this->assign(\'' . $this->_dequote($_args['value']) . '\', $for' . $this->_for_stack . '); ?>';
				return $_result;
				break;
			case '/for':
				$this->_for_stack--;
				return "<?php endfor; ?>";
				break;
			case 'if':
				return $this->_compile_if($arguments);
				break;
			case 'else':
				return "<?php else: ?>";
				break;
			case 'elseif':
				return $this->_compile_if($arguments, true);
				break;
			case '/if':
				return "<?php endif; ?>";
				break;
			case 'assign':
		        	$_args = $this->_parse_arguments($arguments);
				if (!isset($_args['name']))
					$this->trigger_error("missing 'name' attribute in 'assign'", E_USER_ERROR, __FILE__, __LINE__);
				if (!isset($_args['value']))
					$this->trigger_error("missing 'value' attribute in 'assign'", E_USER_ERROR, __FILE__, __LINE__);
				return '<?php $this->assign(\'' . $this->_dequote($_args['name']) . '\', ' . $_args['value'] . '); ?>';
				break;
			case 'switch':
				$_args = $this->_parse_arguments($arguments);
				if (!isset($_args['from']))
					$this->trigger_error("missing 'from' attribute in 'switch'", E_USER_ERROR, __FILE__, __LINE__);
				array_push($this->_switch_stack, array("matched" => false, "var" => $this->_dequote($_args['from'])));
				return;
				break;
			case '/switch':
				array_pop($this->_switch_stack);
				return '<?php break; endswitch; ?>';
				break;
			case 'case':
				if (count($this->_switch_stack) > 0) {
					$_result = "<?php ";
					$_args = $this->_parse_arguments($arguments);
					$_index = count($this->_switch_stack) - 1;
					if (!$this->_switch_stack[$_index]["matched"]) {
						$_result .= 'switch(' . $this->_switch_stack[$_index]["var"] . '): ';
						$this->_switch_stack[$_index]["matched"] = true;
					} else {
						$_result .= 'break; ';
					}
					if (!empty($_args['value']))
						$_result .= 'case '.$_args['value'].': ';
					else
						$_result .= 'default: ';
					return $_result . ' ?>';
				} else {
					$this->trigger_error("unexpected 'case', 'case' can only be in a 'switch'", E_USER_ERROR, __FILE__, __LINE__);
				}
				break;
			default:
				return '';
				//$this->trigger_error($function." function does not exist", E_USER_ERROR, __FILE__, __LINE__);
				break;
		}
	}

	function _compile_if($arguments, $elseif = false) {
		$_result	= "";
		$_match		= array();
		$_args		= array();
		$_is_arg_stack	= array();

		// extract arguments from the equation
		preg_match_all('/(?>(' . $this->_var_regexp . '|\/?' . $this->_func_regexp . ')(?:' . $this->_mod_regexp . '*)?|\-?0[xX][0-9a-fA-F]+|\-?\d+(?:\.\d+)?|\.\d+|!==|===|==|!=|<>|<<|>>|<=|>=|\&\&|\|\||\(|\)|,|\!|\^|=|\&|\~|<|>|\%|\+|\-|\/|\*|\@|\b\w+\b|\S+)/x', $arguments, $_match);
		$_args = $_match[0];

		// make sure we have balanced parenthesis
		$_args_count = array_count_values($_args);
		if(isset($_args_count['(']) && $_args_count['('] != $_args_count[')']) {
			$this->trigger_error("unbalanced parenthesis in if statement", E_USER_ERROR, __FILE__, __LINE__);
		}

		for ($i = 0, $for_max = count($_args); $i < $for_max; $i++) {
			$_arg = &$_args[$i];
			switch (strtolower($_arg)) {
				case '!':
				case '%':
				case '!==':
				case '==':
				case '===':
				case '>':
				case '<':
				case '!=':
				case '<>':
				case '<<':
				case '>>':
				case '<=':
				case '>=':
				case '&&':
				case '||':
				case '^':
				case '&':
				case '~':
				case ')':
				case ',':
				case '+':
				case '-':
				case '*':
				case '/':
				case '@':
					break;					
				case 'eq':
					$_arg = '==';
					break;
				case 'ne':
				case 'neq':
					$_arg = '!=';
					break;
				case 'lt':
					$_arg = '<';
					break;
				case 'le':
				case 'lte':
					$_arg = '<=';
					break;
				case 'gt':
					$_arg = '>';
					break;
				case 'ge':
				case 'gte':
					$_arg = '>=';
					break;
				case 'and':
					$_arg = '&&';
					break;
				case 'or':
					$_arg = '||';
					break;
				case 'not':
					$_arg = '!';
					break;
				case 'mod':
					$_arg = '%';
					break;
				case '(':
					array_push($_is_arg_stack, $i);
					break;
				default:
					preg_match('/(?:(' . $this->_var_regexp . '|' . $this->_func_regexp . ')(' . $this->_mod_regexp . '*)(?:\s*[,\.]\s*)?)(?:\s+(.*))?/xs', $_arg, $_match);
					if ($_match[0]{0} == '$' || ($_match[0]{0} == '#' && $_match[0]{strlen($_match[0]) - 1} == '#') || $_match[0]{0} == "'" || $_match[0]{0} == '"') {
						// process a variable
						$_arg = $this->_parse_variables(array($_match[1]), array($_match[2]));
					} elseif (is_numeric($_arg)) {
						// pass the number through
					} elseif (function_exists($_match[0]) || $_match[0] == "empty" || $_match[0] == "isset" || $_match[0] == "unset" || strtolower($_match[0]) == "true" || strtolower($_match[0]) == "false" || strtolower($_match[0]) == "null") {
						// pass the function through
					} else {
						$this->trigger_error("unidentified token '$_arg'", E_USER_ERROR, __FILE__, __LINE__);
					}
					break;
			}
		}

		if ($elseif) {
			return '<?php elseif ('.implode(' ', $_args).'): ?>';
		} else {
			return '<?php if ('.implode(' ', $_args).'): ?>';
		}
		return $_result;
	}

	function _dequote($string) {
		if (($string{0} == "'" || $string{0} == '"') && $string{strlen($string)-1} == $string{0})
			return substr($string, 1, -1);
		else
			return $string;
	}

	function _parse_arguments($arguments) {
		$_match		= array();
		$_result	= array();
		$_variables	= array();
		preg_match_all('/(?:' . $this->_qstr_regexp . ' | (?>[^"\'=\s]+))+|[=]/x', $arguments, $_match);
		/*
		   Parse state:
		     0 - expecting attribute name
		     1 - expecting '='
		     2 - expecting attribute value (not '=')
		*/
		$state = 0;
		foreach($_match[0] as $value) {
			switch($state) {
				case 0:
					// valid attribute name
					if (is_string($value)) {
						$a_name = $value;
						$state = 1;
					} else {
						$this->trigger_error("invalid attribute name: '$token'", E_USER_ERROR, __FILE__, __LINE__);
					}
					break;
				case 1:
					if ($value == '=') {
						$state = 2;
					} else {
						$this->trigger_error("expecting '=' after '$last_value'", E_USER_ERROR, __FILE__, __LINE__);
					}
					break;
				case 2:
					if ($value != '=') {
						if ($value == 'yes' || $value == 'on' || $value == 'true') {
							$value = true;
						} elseif ($value == 'no' || $value == 'off' || $value == 'false') {
							$value = false;
						} elseif ($value == 'null') {
							$value = null;
						}

						if(!preg_match_all('/(?:(' . $this->_var_regexp . ')(' . $this->_mod_regexp . '*))(?:\s+(.*))?/xs', $value, $_variables)) {
							$_result[$a_name] = $value;
						} else {
							$_result[$a_name] = $this->_parse_variables($_variables[1], $_variables[2]);
						}
						$state = 0;
					} else {
						$this->trigger_error("'=' cannot be an attribute value", E_USER_ERROR, __FILE__, __LINE__);
					}
					break;
			}
			$last_value = $value;
		}
		if($state != 0) {
			if($state == 1) {
				$this->trigger_error("expecting '=' after attribute name '$last_value'", E_USER_ERROR, __FILE__, __LINE__);
			} else {
				$this->trigger_error("missing attribute value", E_USER_ERROR, __FILE__, __LINE__);
			}
		}
		return $_result;
	}

	function _parse_variables($variables, $modifiers) {
		$_result = "";
		foreach($variables as $key => $value) {
			$tag_variable = trim($variables[$key]);

			$_result .= $this->_parse_variable($tag_variable).'.';
		}
		return substr($_result, 0, -1);
	}

	function _parse_variable($variable) {
		// replace variable with value
		if ($variable{0} == "\$") {
			// replace the variable
			return $this->_compile_variable($variable);
		} elseif ($variable{0} == '#') {
			$variable=substr($variable, 1);
			$variable='\$'.substr($variable, 0,count($variable)-1);
			return $this->_compile_variable($variable);	
		} elseif ($variable{0} == '"') {
			// expand the quotes to pull any variables out of it
			// fortunately variables inside of a quote aren't fancy, no modifiers, no quotes
			//   just get everything from the $ to the ending space and parse it
			// if the $ is escaped, then we won't expand it
			$_result = "";
			preg_match_all('/(?:[^\\\]' . $this->_dvar_regexp . ')/', substr($variable, 1, -1), $_expand);
			$_expand = array_unique($_expand[0]);
			foreach($_expand as $key => $value) {
				$_expand[$key] = trim($value);
				if (strpos($_expand[$key], '$') > 0) {
					$_expand[$key] = substr($_expand[$key], strpos($_expand[$key], '$'));
				}
			}
			$_result = $variable;
			foreach($_expand as $value) {
				$value = trim($value);
				$_result = str_replace($value, '" . ' . $this->_parse_variable($value) . ' . "', $_result);
			}
			return $_result;
		} elseif ($variable{0} == "'") {
			// return the value just as it is
			return $variable;
		} else {
			// return it as is; i believe that there was a reason before that i did not just return it as is,
			// but i forgot what that reason is ...
			// the reason i return the variable 'as is' right now is so that unquoted literals are allowed
			return $variable;
		}
	}

	function _compile_variable($variable) {
		$_result	= "";

		// remove the $
		$variable = substr($variable, 1);

		// get [foo] and .foo and (...) pieces			
		preg_match_all('!(?:^\w+)|(?:' . $this->_var_bracket_regexp . ')|\.\$?\w+|\S+!', $variable, $_match);
		$variable = $_match[0];
		$var_name = array_shift($variable);

		if ($var_name == '_TPL') {
			if ($variable[0]{0} == '[') {
				switch($variable[0]) {
					case '[GET]':
						$_result = "\$_GET";
						break;
					case '[POST]':
						$_result = "\$_POST";
						break;
					case '[COOKIE]':
						$_result = "\$_COOKIE";
						break;
					case '[ENV]':
						$_result = "\$_ENV";
						break;
					case '[SERVER]':
						$_result = "\$_SERVER";
						break;
					case '[SESSION]':
						$_result = "\$_SESSION";
						break;
					case '[NOW]':
						$_result = "time()";
						break;
					case '[GLOBAL]':
						$_result = "\$GLOBALS";
						break;
					case '[YEAR]':
						$_result ="date('Y', time())";
						break;
					default:
						$this->trigger_error('$' . $var_name.implode('', $variable) . ' is an invalid $_TPL reference', E_USER_ERROR, __FILE__, __LINE__);
						break;
				}
				array_shift($variable);
			} else {
				$this->trigger_error('$' . $var_name.implode('', $variable) . ' is an invalid $_TPL reference', E_USER_ERROR, __FILE__, __LINE__);
			}
		} else {
			$_result = "\$this->_vars['$var_name']";
		}

		foreach ($variable as $var) {
			if ($var{0} == '[') {
				$var = substr($var, 1, -1);
				if (is_numeric($var)) {
					$_result .= "[$var]";
				} elseif ($var{0} == '$') {
					$_result .= "[" . $this->_compile_variable($var) . "]";

				} else {
					$_result .= "['$var']";
				}
			} else {
				$this->trigger_error('$' . $var_name.implode('', $variable) . ' is an invalid reference', E_USER_ERROR, __FILE__, __LINE__);
			}
		}
		return $_result;
	}
}
?>